using System;
using PropertyExpression.Common;

namespace PropertyExpression.ReactiveRelation
{
    internal class CalcExecutor<TCalcContext> : ICalcExecutor
    {
        private readonly CalcBatchManager calcBatchManager;
        private readonly Func<CalcStorage<TCalcContext>> calcStorageFunc;
        private readonly Func<TCalcContext> calcContextFunc;

        private bool inCalculateProcess;

        public CalcExecutor(Func<CalcStorage<TCalcContext>> calcStorageFunc, Func<TCalcContext> calcContextFunc)
        {
            this.calcStorageFunc = calcStorageFunc;
            this.calcContextFunc = calcContextFunc;
            calcBatchManager = new CalcBatchManager(this);
        }

        public void Calculate(object sender, Func<string> propertyNameFunc, object previousValue, 
            Func<IOneToManyCollectionNewItemProcessor> oneToManyCollectionNewItemProcessorFunc)
        {
            if (inCalculateProcess) return;

            Calculate(
                calculatedValueCache =>
                calcStorageFunc().ReverseDependencyManager.SetIsCalculatedCells(
                    sender,
                    propertyNameFunc(),
                    calculatedValueCache,
                    new PreviousValueProcessor(
                        () => new PreviousValueProvider(
                                  () => sender,
                                  propertyNameFunc,
                                  () => previousValue
                                  )
                                  .AsOptionalResult<IPreviousValueProvider>()
                        ),
                    (newEntity, propertyName) =>
                    sender == newEntity && propertyNameFunc() == propertyName
                    ),
                oneToManyCollectionNewItemProcessorFunc
                );
        }

        public void CalcBatch(Action action)
        {
            CalcBatch(action.ToFunc());
        }

        public TResult CalcBatch<TResult>(Func<TResult> func)
        {
            return calcBatchManager.CalcBatch(
                func, () => new OneToManyCollectionNewItemProcessor(
                                OptionalResult.AsOptionalResult<IOneToManyCollectionNewItemProvider>)
                );
        }

        public void CalculateProperties(object entity)
        {
            Calculate(
                calculatedValueCache =>
                calcStorageFunc().DependencyManager.
                    CalculateProperties(
                    entity,
                    calculatedValueCache,
                    new PreviousValueProcessor(
                        OptionalResult.AsOptionalResult<IPreviousValueProvider>)
                    ),
                () => new OneToManyCollectionNewItemProcessor(
                          OptionalResult.AsOptionalResult<IOneToManyCollectionNewItemProvider>)
                );
        }

        private void Calculate(Action<CalculatedValueCache<TCalcContext>> action, 
            Func<IOneToManyCollectionNewItemProcessor> oneToManyCollectionNewItemProcessorFunc)
        {
            calcBatchManager.CalcBatch(action, oneToManyCollectionNewItemProcessorFunc);
        }

        private class CalcBatchManager
        {
            private readonly CalcExecutor<TCalcContext> parent;
            private CalculatedValueCache<TCalcContext> calculatedValueCache;

            public CalcBatchManager(CalcExecutor<TCalcContext> parent)
            {
                this.parent = parent;
            }

            public void CalcBatch(Action<CalculatedValueCache<TCalcContext>> action, 
                Func<IOneToManyCollectionNewItemProcessor> oneToManyCollectionNewItemProcessorFunc)
            {
                Action tempAction = () => action(calculatedValueCache);
                CalcBatch(tempAction.ToFunc(), oneToManyCollectionNewItemProcessorFunc);
            }

            public TResult CalcBatch<TResult>(Func<TResult> func, 
                Func<IOneToManyCollectionNewItemProcessor> oneToManyCollectionNewItemProcessorFunc)
            {
                if (calculatedValueCache != null)
                {
                    return func();
                }
                else
                {
                    try
                    {
                        calculatedValueCache = new CalculatedValueCache<TCalcContext>();

                        var result = func();

                        try
                        {
                            parent.inCalculateProcess = true;
                            var bindContext = new BindContext(this);
                            calculatedValueCache.Calculate(
                                oneToManyCollectionNewItemProcessorFunc(), 
                                () => bindContext);
                        }
                        finally
                        {
                            parent.inCalculateProcess = false;
                        }

                        return result;
                    }
                    finally
                    {
                        calculatedValueCache = null;
                    }
                }                
            }

            private class BindContext : IBindContext<TCalcContext>
            {
                private readonly CalcBatchManager parent;
                private readonly FuncCache funcCache = new FuncCache();

                public BindContext(CalcBatchManager parent)
                {
                    this.parent = parent;
                }

                public FuncCache FuncCache
                {
                    get { return funcCache; }
                }

                public TCalcContext CalcContext
                {
                    get { return parent.parent.calcContextFunc(); }
                }
            }
        }

        private class PreviousValueProcessor : IPreviousValueProcessor
        {
            private readonly Func<IOptionalResult<IPreviousValueProvider>> func;

            public PreviousValueProcessor(Func<IOptionalResult<IPreviousValueProvider>> func)
            {
                this.func = func;
            }

            public IOptionalResult<IPreviousValueProvider> Process()
            {
                return func();
            }
        }

        private class PreviousValueProvider : IPreviousValueProvider
        {
            private readonly Func<object> senderFunc;
            private readonly Func<string> propertyNameFunc;
            private readonly Func<object> previousValueFunc;

            public PreviousValueProvider(
                Func<object> senderFunc,
                Func<string> propertyNameFunc,
                Func<object> previousValueFunc)
            {
                this.senderFunc = senderFunc;
                this.propertyNameFunc = propertyNameFunc;
                this.previousValueFunc = previousValueFunc;
            }

            public object Sender
            {
                get { return senderFunc(); }
            }

            public string PropertyName
            {
                get { return propertyNameFunc(); }
            }

            public object PreviousValue
            {
                get { return previousValueFunc(); }
            }
        }
    }
}